/******************************************************************************* * Copyright 2014 Tobias Welther * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package de.tobiyas.racesandclasses.traitcontainer.traits.arrows; import static de.tobiyas.racesandclasses.translation.languages.Keys.arrow_change; import java.util.List; import java.util.concurrent.Callable; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Arrow; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.Projectile; import org.bukkit.event.Event; import org.bukkit.event.block.Action; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityShootBowEvent; import org.bukkit.event.entity.ProjectileHitEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.metadata.LazyMetadataValue; import org.bukkit.metadata.MetadataValue; import org.bukkit.scheduler.BukkitRunnable; import de.tobiyas.racesandclasses.RacesAndClasses; import de.tobiyas.racesandclasses.APIs.LanguageAPI; import de.tobiyas.racesandclasses.datacontainer.arrow.ArrowManager; import de.tobiyas.racesandclasses.datacontainer.traitholdercontainer.TraitHolderCombinder; import de.tobiyas.racesandclasses.eventprocessing.eventresolvage.EventWrapper; import de.tobiyas.racesandclasses.eventprocessing.eventresolvage.PlayerAction; import de.tobiyas.racesandclasses.playermanagement.player.RaCPlayer; import de.tobiyas.racesandclasses.playermanagement.player.RaCPlayerManager; import de.tobiyas.racesandclasses.playermanagement.playerdisplay.scoreboard.PlayerRaCScoreboardManager.SBCategory; import de.tobiyas.racesandclasses.traitcontainer.interfaces.TraitResults; import de.tobiyas.racesandclasses.traitcontainer.interfaces.annotations.configuration.TraitConfigurationField; import de.tobiyas.racesandclasses.traitcontainer.interfaces.annotations.configuration.TraitConfigurationNeeded; import de.tobiyas.racesandclasses.traitcontainer.interfaces.annotations.configuration.TraitEventsUsed; import de.tobiyas.racesandclasses.traitcontainer.interfaces.markerinterfaces.CostType; import de.tobiyas.racesandclasses.traitcontainer.interfaces.markerinterfaces.Trait; import de.tobiyas.racesandclasses.traitcontainer.interfaces.markerinterfaces.TraitWithCost; import de.tobiyas.racesandclasses.traitcontainer.traits.magic.AbstractMagicSpellTrait; import de.tobiyas.racesandclasses.traitcontainer.traits.pattern.AbstractActivatableTrait; import de.tobiyas.racesandclasses.util.bukkit.versioning.compatibility.CompatibilityModifier; import de.tobiyas.racesandclasses.util.friend.EnemyChecker; import de.tobiyas.racesandclasses.util.traitutil.TraitConfiguration; import de.tobiyas.racesandclasses.util.traitutil.TraitConfigurationFailedException; import de.tobiyas.racesandclasses.vollotile.ParticleContainer; import de.tobiyas.racesandclasses.vollotile.Vollotile; import de.tobiyas.util.schedule.DebugBukkitRunnable; public abstract class AbstractArrow extends AbstractActivatableTrait implements TraitWithCost { protected static final String BOUND_TO_BOW_PATH = "boundToBow"; protected static final String INITIAL_DAMAGE_PATH = "initialDamage"; protected static final String ARROW_PARTICLE_PATH_PATH = "arrowParticlePath"; protected RacesAndClasses plugin = RacesAndClasses.getPlugin(); /** * The duration to use. */ protected int duration; /** * Total damage var. */ protected double totalDamage; /** * The initial damage to deal. */ protected double initialDamage = -1; /** * If the trait is bound to the Bow or on-use. */ protected boolean boundToBow = true; ////Mana part: /** * The Cost of the Spell. * * It has the default Cost of 0. */ protected double cost = 0; /** * The Material for casting with {@link CostType#ITEM} */ protected Material materialForCasting = Material.FEATHER; /** * The CostType of the Spell. * * It has the Default CostType: {@link CostType#MANA}. */ protected CostType costType = CostType.MANA; /** * The Material damage for casting. */ protected byte materialDamageForCasting = 0; /** * The Material Name for casting. */ protected String materialNameForCasting = null; /** * The Particles to show when following the arrow. */ protected ParticleContainer arrowPathParticles = null; @TraitConfigurationNeeded(fields = { @TraitConfigurationField(fieldName = BOUND_TO_BOW_PATH, classToExpect = Boolean.class, optional = true), @TraitConfigurationField(fieldName = INITIAL_DAMAGE_PATH, classToExpect = double.class, optional = true), @TraitConfigurationField(fieldName = ARROW_PARTICLE_PATH_PATH, classToExpect = ParticleContainer.class, optional = true), @TraitConfigurationField(fieldName = AbstractMagicSpellTrait.COST_PATH, classToExpect = Double.class, optional = true), @TraitConfigurationField(fieldName = AbstractMagicSpellTrait.COST_TYPE_PATH, classToExpect = String.class, optional = true), @TraitConfigurationField(fieldName = AbstractMagicSpellTrait.ITEM_TYPE_PATH, classToExpect = Material.class, optional = true), @TraitConfigurationField(fieldName = AbstractMagicSpellTrait.ITEM_DAMAGE_PATH, classToExpect = Integer.class, optional = true), @TraitConfigurationField(fieldName = AbstractMagicSpellTrait.ITEM_NAME_PATH, classToExpect = String.class, optional = true) }) @Override public void setConfiguration(TraitConfiguration configMap) throws TraitConfigurationFailedException { super.setConfiguration(configMap); //Bow related stuff: this.boundToBow = configMap.getAsBool(BOUND_TO_BOW_PATH, true); this.initialDamage = configMap.getAsDouble(INITIAL_DAMAGE_PATH, -1); this.arrowPathParticles = configMap.getAsParticleContainer(ARROW_PARTICLE_PATH_PATH, null); //Magic costs: cost = configMap.getAsDouble(AbstractMagicSpellTrait.COST_PATH, 0); if(configMap.containsKey(AbstractMagicSpellTrait.COST_TYPE_PATH)){ String costTypeName = configMap.getAsString(AbstractMagicSpellTrait.COST_TYPE_PATH); costType = CostType.tryParse(costTypeName); if(costType == null){ throw new TraitConfigurationFailedException(getName() + " is incorrect configured. costType could not be read."); } if(costType == CostType.ITEM){ if(!configMap.containsKey(AbstractMagicSpellTrait.ITEM_TYPE_PATH)){ throw new TraitConfigurationFailedException(getName() + " is incorrect configured. 'costType' was ITEM but no Item is specified at 'item'."); } materialForCasting = configMap.getAsMaterial(AbstractMagicSpellTrait.ITEM_TYPE_PATH); if(materialForCasting == null){ throw new TraitConfigurationFailedException(getName() + " is incorrect configured." + " 'costType' was ITEM but the item read is not an Item. Items are CAPITAL. " + "See 'https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html' for all Materials. " + "Alternative use an ItemID."); } materialDamageForCasting = (byte) configMap.getAsInt(AbstractMagicSpellTrait.ITEM_DAMAGE_PATH, 0); materialNameForCasting = configMap.getAsString(AbstractMagicSpellTrait.ITEM_NAME_PATH, null); } } } @Override public boolean canBeTriggered(EventWrapper wrapper){ Event event = wrapper.getEvent(); if(!(event instanceof PlayerInteractEvent || event instanceof EntityShootBowEvent || event instanceof ProjectileHitEvent || event instanceof EntityDamageByEntityEvent)) return false; RaCPlayer player = wrapper.getPlayer(); //Change ArrowType if(event instanceof PlayerInteractEvent){ PlayerInteractEvent Eevent = (PlayerInteractEvent) event; if(!(Eevent.getAction() == Action.LEFT_CLICK_AIR)) return false; if(!isThisArrow(player)) return false; if(!TraitHolderCombinder.checkContainer(player, this)) return false; if(player.getPlayer().getItemInHand().getType() != Material.BOW) return false; return true; } //Projectile launch if(event instanceof EntityShootBowEvent){ EntityShootBowEvent Eevent = (EntityShootBowEvent) event; if(Eevent.getEntity().getType() != EntityType.PLAYER) return false; if(!TraitHolderCombinder.checkContainer(player, this)) return false; if(!isThisArrow(player)) return false; return true; } //Arrow Hit Location if(event instanceof ProjectileHitEvent){ ProjectileHitEvent Eevent = (ProjectileHitEvent) event; if(Eevent.getEntityType() != EntityType.ARROW) return false; final Projectile arrow = (Projectile) Eevent.getEntity(); if(CompatibilityModifier.Shooter.getShooter(arrow) == null) return false; List<MetadataValue> metaValues = arrow.getMetadata(ARROW_META_KEY); if(arrow.getMetadata(ARROW_META_KEY).isEmpty()) return false; boolean found = false; for(MetadataValue value : metaValues){ if(getDisplayName().equals(value.value())){ found = true; break; } } if(!found) return false; //Remove the meta to not leak them. //We have to schedule this 1 tick to future, since this event is called BEFORE the damage event in 1.8+. removeMetadataNextTick(arrow); LivingEntity shooter = CompatibilityModifier.Shooter.getShooter(arrow); if(shooter.getType() != EntityType.PLAYER) return false; RaCPlayer realPlayer = RaCPlayerManager.get().getPlayer((Player) shooter); if(!TraitHolderCombinder.checkContainer(realPlayer, this)) return false; if(!isThisArrow(realPlayer)) return false; return true; } //Arrow Hits target if(event instanceof EntityDamageByEntityEvent){ EntityDamageByEntityEvent Eevent = (EntityDamageByEntityEvent) event; if(Eevent.getDamager().getType() != EntityType.ARROW) return false; Arrow realArrow = (Arrow) Eevent.getDamager(); LivingEntity shooter = CompatibilityModifier.Shooter.getShooter(realArrow); if(shooter == null || realArrow == null || realArrow.isDead()) return false; if(shooter.getType() != EntityType.PLAYER) return false; if(Eevent.getEntity() == shooter && realArrow.getTicksLived() < 5) return false; if(!TraitHolderCombinder.checkContainer(player, this)) return false; if(realArrow.getMetadata(ARROW_META_KEY).isEmpty()) return false; List<MetadataValue> metaValues = realArrow.getMetadata(ARROW_META_KEY); boolean found = false; for(MetadataValue value : metaValues){ if(getDisplayName().equals(value.value())){ found = true; break; } } if(!found) return false; //Remove the meta to not leak them. realArrow.removeMetadata(ARROW_META_KEY, plugin); //you can not hit your allies. if(EnemyChecker.areAllies(realArrow, Eevent.getEntity())) return false; if(!isThisArrow(player)) return false; return true; } return false; } /** * Checks if the Arrow is active at the moment from the passed player * * @param player to check * @return true if active, false if not. */ private boolean isThisArrow(RaCPlayer player){ ArrowManager arrowManager = player.getArrowManager(); AbstractArrow arrow = arrowManager.getCurrentArrow(); if(arrow == null || arrow != this) return false; return true; } /** * The Meta Key for the Arrow to search for. */ private static final String ARROW_META_KEY = "arrowType"; @Override public TraitResults trigger(EventWrapper eventWrapper) { Event event = eventWrapper.getEvent(); TraitResults result = new TraitResults(); //Change ArrowType if(event instanceof PlayerInteractEvent){ changeArrowType(eventWrapper.getPlayer()); return result.setTriggered(false); } //Projectile launch if(event instanceof EntityShootBowEvent){ EntityShootBowEvent Eevent = (EntityShootBowEvent) event; Arrow arrow = (Arrow) Eevent.getProjectile(); arrow.setMetadata(ARROW_META_KEY , new LazyMetadataValue(plugin, new Callable<Object>() { @Override public Object call() throws Exception { return getDisplayName(); } })); boolean triggered = onShoot(Eevent); if(triggered){ //Do not forget to remove the Cost for spells: eventWrapper.getPlayer().getSpellManager().removeCost(this); addParticleTask(arrow); } return result.setTriggered(triggered).setSetCooldownOnPositiveTrigger(triggered).setRemoveCostsAfterTrigger(triggered); } //Arrow Hit Location if(event instanceof ProjectileHitEvent){ ProjectileHitEvent Eevent = (ProjectileHitEvent) event; boolean change = onHitLocation(Eevent); return result.setTriggered(change); } //Arrow Hits target if(event instanceof EntityDamageByEntityEvent){ EntityDamageByEntityEvent Eevent = (EntityDamageByEntityEvent) event; boolean change = onHitEntity(Eevent); Eevent.getDamager().remove(); double modInitDamage = modifyToPlayer(eventWrapper.getPlayer(), initialDamage, "initialDamage"); if(modInitDamage > 0) Eevent.setDamage(modInitDamage); return result.setTriggered(change); } return result.setTriggered(false); } /** * Adds a task to show particles on the Arrow. * @param arrow to show. */ private void addParticleTask(final Arrow arrow) { if(arrow == null || this.arrowPathParticles == null) return; new BukkitRunnable() { @Override public void run() { if(arrow.isDead() || !arrow.isValid() || arrow.getVelocity().lengthSquared() < 0.2){ this.cancel(); return; } Vollotile.get().sendOwnParticleEffectToAll(arrowPathParticles, arrow.getLocation()); } }.runTaskTimer(plugin, 2, 2); } /** * Changes to the next arrow. */ protected void changeArrowType(RaCPlayer player){ ArrowManager arrowManager = player.getArrowManager(); AbstractArrow arrow = arrowManager.getCurrentArrow(); if(arrow == null || arrow != this) return; boolean forward = !player.getPlayer().isSneaking(); AbstractArrow newArrow = forward ? arrowManager.nextArrow() : arrowManager.previousArrow(); if(newArrow != null && newArrow != arrow){ if(!plugin.getConfigManager().getGeneralConfig().isConfig_enable_permanent_scoreboard()){ player.getScoreboardManager().updateSelectAndShow(SBCategory.Arrows); } LanguageAPI.sendTranslatedMessage(player, arrow_change, "trait_name", newArrow.getDisplayName()); } } /** * This is called when a Player shoots an Arrow with this ArrowTrait present * * @param event that was triggered * @return true if a cooldown should be triggered */ protected abstract boolean onShoot(EntityShootBowEvent event); /** * This is triggered when the Player Hits an Entity with it's arrow * * @param event that triggered the event * @return true if an Cooldown should be triggered */ protected abstract boolean onHitEntity(EntityDamageByEntityEvent event); /** * This is triggered when the Player hits an Location * * @param event that triggered the event * @return true if an Cooldown should be triggered */ protected abstract boolean onHitLocation(ProjectileHitEvent event); /** * Returns the name of the Arrow type * * @return */ protected abstract String getArrowName(); @Override public String getDisplayName() { String superDisplayName = super.getDisplayName(); if(superDisplayName.equals(getName())){ return getArrowName(); } return superDisplayName; } @Override public boolean isBetterThan(Trait trait){ if(trait.getClass() != this.getClass()) return false; //TODO Not sure about this... return false; } @Override public TraitResults trigger(RaCPlayer player) { Player realPlayer = player.getPlayer(); if(realPlayer == null || !realPlayer.isOnline()) return TraitResults.False(); Arrow arrow = realPlayer.launchProjectile(Arrow.class); if(arrow != null){ ItemStack item = new ItemStack(Material.BOW); item.addEnchantment(Enchantment.ARROW_INFINITE, 1); //Do not forget to remove the Cost for spells: player.getSpellManager().removeCost(this); onShoot(new EntityShootBowEvent(realPlayer, item, arrow, 1f)); return TraitResults.True(); } return TraitResults.False(); } @Override public boolean triggerButHasUplink(EventWrapper wrapper) { if(wrapper.getPlayerAction() == PlayerAction.INTERACT_BLOCK || wrapper.getPlayerAction() == PlayerAction.INTERACT_BLOCK){ changeArrowType(wrapper.getPlayer()); return true; } if(wrapper.getEvent() instanceof ProjectileHitEvent){ //Bypass the Uplink. if(canBeTriggered(wrapper)) trigger(wrapper); return true; } if(wrapper.getEvent() instanceof EntityDamageByEntityEvent){ //Bypass the Uplink. if(canBeTriggered(wrapper)) trigger(wrapper); return true; } if(wrapper.getPlayerAction() == PlayerAction.DO_DAMAGE){ return true; } return false; } @Override public boolean isStackable(){ return false; } @TraitEventsUsed(registerdClasses = { EntityDamageByEntityEvent.class, PlayerInteractEvent.class, EntityShootBowEvent.class, ProjectileHitEvent.class }) @Override public void generalInit() { } @Override public boolean notifyTriggeredUplinkTime(EventWrapper wrapper) { if(wrapper.getPlayer().getArrowManager().getCurrentArrow() != this) return false; return super.notifyTriggeredUplinkTime(wrapper); } @Override public boolean isBindable() { return !boundToBow; } @Override public double getCost(RaCPlayer player){ int level = player.getLevelManager().getCurrentLevel(); return this.skillConfig.getCastCostForLevel(level, modifyToPlayer(player, cost, "cost")); } @Override public CostType getCostType(){ return costType; } @Override public Material getCastMaterialType(RaCPlayer player) { int level = player.getLevelManager().getCurrentLevel(); return this.skillConfig.getCastMaterialForLevel(level, this.materialForCasting); } @Override public short getCastMaterialDamage(RaCPlayer player) { int level = player.getLevelManager().getCurrentLevel(); return this.skillConfig.getCastMaterialDamageForLevel(level, this.materialDamageForCasting); } @Override public String getCastMaterialName(RaCPlayer player) { int level = player.getLevelManager().getCurrentLevel(); return this.skillConfig.getCastMaterialNameForLevel(level, this.materialNameForCasting); } @Override public void triggerButDoesNotHaveEnoghCostType(EventWrapper wrapper) {} @Override public boolean needsCostCheck(EventWrapper wrapper) { return wrapper.getEvent() instanceof EntityShootBowEvent; } /** * Removes the Metadata from the Projectile in the Next tick. * @param pro to remove. */ private void removeMetadataNextTick(final Projectile pro){ new DebugBukkitRunnable("ArrowMetaRemover") { @Override protected void runIntern() { try{ pro.removeMetadata(ARROW_META_KEY, plugin); }catch(Throwable exp){} } }; } }